package dk.au.cs.peterbn.processing;

import com.martiansoftware.jsap.JSAPException;
import com.sun.source.util.Trees;
import com.sun.tools.javac.processing.JavacProcessingEnvironment;
import com.sun.tools.javac.util.BaseFileObject;
import dk.au.cs.peterbn.annotations.Pattern;
import dk.au.cs.peterbn.annotations.Patterns;
import dk.au.cs.peterbn.patterns.DesignPattern;
import dk.au.cs.peterbn.patterns.TypeRole;
import dk.au.cs.peterbn.processing.compilationJobs.*;
import dk.au.cs.peterbn.processing.instanceJobs.CheckAllTypeRolesPresent;
import dk.au.cs.peterbn.processing.instanceJobs.CheckFieldReferences;
import dk.au.cs.peterbn.spoonprocessing.CallSiteProcessor;
import dk.au.cs.peterbn.spoonprocessing.CollectingEnvironment;
import dk.au.cs.peterbn.spoonprocessing.SpoonLauncher;
import org.reflections.Reflections;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.annotation.processing.*;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.AnnotationMirror;
import javax.lang.model.element.Element;
import javax.lang.model.element.ExecutableElement;
import javax.lang.model.element.TypeElement;
import javax.lang.model.util.ElementFilter;
import javax.lang.model.util.Elements;
import javax.lang.model.util.Types;
import javax.tools.Diagnostic;
import javax.tools.JavaFileObject;
import java.io.File;
import java.util.*;

import static javax.lang.model.util.ElementFilter.typesIn;

/**
 * Simple annotation processor that checks that all elements of a pattern is present in the required multiplicities
 */
@SupportedAnnotationTypes("dk.au.cs.peterbn.annotations.*")
@SupportedSourceVersion(SourceVersion.RELEASE_6)
public class PatternsProcessor extends AbstractProcessor {

    private static final Logger logger = LoggerFactory.getLogger(PatternsProcessor.class);

    private Map<String, PatternInstance> patternInstances = new HashMap<String, PatternInstance>();
    private ReflectionsFactory reflectionsFactory;
    private ProcessingUtil util;
    private String classPath;

    public PatternsProcessor() {
        this(new ReflectionsFactory());
    }

    public PatternsProcessor(ReflectionsFactory reflectionsFactory) {
        this.reflectionsFactory = reflectionsFactory;
    }

    @Override
    public void init(ProcessingEnvironment processingEnv) {
        super.init(processingEnv);
        logger.info("Running annotation processor");
        Elements elementUtils = processingEnv.getElementUtils();
        Types typeUtils = processingEnv.getTypeUtils();
        //load reflections to work on already compiled elements
        CompilerOptions compilerOptions = null;
        if (processingEnv instanceof JavacProcessingEnvironment) {
            JavacProcessingEnvironment env = (JavacProcessingEnvironment) processingEnv;
            compilerOptions = new CompilerOptions(env);
            classPath = compilerOptions.getClassPath();
        }
        Reflections reflections = reflectionsFactory.getReflections(compilerOptions);
        logger.debug("Constructing ProcessingUtil");
        util = new ProcessingUtil(processingEnv, elementUtils, reflections, typeUtils);
        logger.debug("Done initializing");
    }


    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment env) {
        if (annotations.isEmpty()) {
            //Quick return if there is no work to do
            return false;
        }
        Map<String, Set<Element>> conflictingPatterns = new HashMap<String, Set<Element>>();

        /*
        Discover the set of pattern instances in the code we are compiling.
        We are not going to check patterns in already compiled code, only use them to check the new patterns.
         */
        //Get the compile elements with patterns
        Set<Element> annotatedWithPattern = new HashSet<Element>(env.getElementsAnnotatedWith(Pattern.class));
        annotatedWithPattern.addAll(env.getElementsAnnotatedWith(Patterns.class));
        for (TypeElement element : typesIn(annotatedWithPattern)) {
            List<AnnotationMirror> patternAnnotations = util.getPatternAnnotationMirrors(element);
            for (AnnotationMirror annotation : patternAnnotations) {
                DesignPattern designPattern = util.getDesignPattern(element, annotation);

                if (designPattern != null) {
                    String patternId = util.getId(annotation);
                    PatternInstance patternInstance = patternInstances.get(patternId);
                    if (patternInstance == null) {
                        patternInstance = new PatternInstance(patternId, designPattern);
                        patternInstances.put(patternId, patternInstance);
                    }
                    Set<Element> conflicts = conflictingPatterns.get(patternInstance.getId());
                    if (!patternInstance.getName().equals(designPattern)) {
                        if (conflicts == null) {
                            conflicts = new HashSet<Element>(patternInstance.getElements());
                            conflictingPatterns.put(patternInstance.getId(), conflicts);
                        }
                        conflicts.add(element);

                    } else {
                        if (conflicts != null) {
                            conflicts.add(element);
                        }
                        patternInstance.addElement(element);
                    }
                }
            }
        }

        for (Map.Entry<String, Set<Element>> stringSetEntry : conflictingPatterns.entrySet()) {
            //Print out lots of error messages if we have duplicate ID's
            for (Element conflictingElement : stringSetEntry.getValue()) {
                util.printMessage(Diagnostic.Kind.ERROR, "Two patterns with same id: " + stringSetEntry.getKey(), conflictingElement);
            }
        }

        //Check that we aren't re-using instance IDs from the classpath
        Set<Class<?>> withPatterns = util.getClassPathTypesWithPatterns();
        for (Class<?> withPattern : withPatterns) {
            Set<Pattern> patterns = util.getAllPatternAnnotations(withPattern);
            for (Pattern pattern : patterns) {
                String id = pattern.id();
                PatternInstance instance = patternInstances.get(id);
                if (instance != null && !instance.getName().getIdentifier().equals(pattern.name())) {
                    util.printMessage(Diagnostic.Kind.ERROR, String.format("The pattern instance id: %s is already used on the classpath by a pattern with identifier %s",id,pattern.name()));
                }
            }

        }

        util.setPatternInstances(patternInstances);


        List<ProcessingJob> jobs = new ArrayList<ProcessingJob>();
        jobs.add(new CheckAllTypeRolesPresent(env,patternInstances,util));
        jobs.add(new CheckAnnotatedSubClasses(env, util));
        jobs.add(new CheckAnnotatedSuperClasses(env, util));
        jobs.add(new CheckFieldReferences(patternInstances,util));
        jobs.add(new CheckAllRolesCorrectTarget(patternInstances, util));
        jobs.add(new CheckAllMemberRolesPresent(patternInstances, util));
        jobs.add(new CheckAllMembersValidIDs(env, util));
        for (Iterator<ProcessingJob> iterator = jobs.iterator(); util.noErrors() && iterator.hasNext(); ) {
            ProcessingJob compilationJob = iterator.next();
            compilationJob.process();
        }
        if (util.noErrors()) {
            runSpoonProcessors(annotations, env);
        }

        return false; //Continue processing
    }

    public boolean runSpoonProcessors(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
        Set<? extends Element> rootElements = roundEnv.getRootElements();
        Set<TypeElement> typeElements = ElementFilter.typesIn(rootElements);

        SpoonLauncher spoonLauncher;
        try {
            spoonLauncher = new SpoonLauncher();
        } catch (JSAPException e) {
            throw new RuntimeException(e);
        }

        spoonLauncher.setCp(classPath);
        CallSiteProcessor processor = new CallSiteProcessor();
        processor.setUtil(util);
        spoonLauncher.addProcessor(processor);

        Trees instance = Trees.instance(processingEnv);
        for (TypeElement typeElement : typeElements) {
            JavaFileObject sourceFile = instance.getPath(typeElement).getCompilationUnit().getSourceFile();
            if (sourceFile instanceof BaseFileObject) {
                BaseFileObject fileObject = (BaseFileObject) sourceFile;
                String path = fileObject.getPath();
                File file = new File(path);
                spoonLauncher.addInputFile(file);
            }
        }
        try {
            spoonLauncher.run();
            List<CollectingEnvironment.ReportItem> reportItems = spoonLauncher.getEnv().getReportItems();
            for (CollectingEnvironment.ReportItem reportItem : reportItems) {
                Diagnostic.Kind kind = null;
                switch (reportItem.severity) {

                    case ERROR:
                        kind = Diagnostic.Kind.ERROR;
                        break;
                    case WARNING:
                        kind = Diagnostic.Kind.WARNING;
                        break;
                    case MESSAGE:
                        kind = Diagnostic.Kind.NOTE;
                        break;
                }
                processingEnv.getMessager().printMessage(kind, reportItem.msg);
            }

        } catch (Exception e) {
            throw new RuntimeException(e);
        }
        return false;
    }


    @Override
    public Iterable<? extends Completion> getCompletions(Element element, AnnotationMirror annotation, ExecutableElement member, String userText) {
        List<Completion> completionList = new ArrayList<Completion>();
        if (isPatternAnnotation(annotation)) {
            if (member.getSimpleName().toString().equals("role")) {
                DesignPattern designPattern = util.getDesignPattern(element, annotation);
                if (designPattern != null) {
                    Collection<TypeRole> types = new ArrayList<TypeRole>(designPattern.getTypes());
                    if (userText != null && userText.length() > 0) {
                        for (Iterator<TypeRole> iterator = types.iterator(); iterator.hasNext(); ) {
                            TypeRole next = iterator.next();
                            if (!(next.getClass().getSimpleName().startsWith(userText) || next.getClass().getCanonicalName().startsWith(userText))) {
                                iterator.remove();
                            }
                        }
                    }
                    for (TypeRole type : types) {
                        completionList.add(new RoleCompletion(type));
                    }
                }
            }
        }
        return completionList;
    }

    private boolean isPatternAnnotation(AnnotationMirror annotation) {
        return annotation.getAnnotationType().toString().equals(Pattern.class.getCanonicalName());
    }

    public static class RoleCompletion implements Completion {

        private String value;
        private String pattern;

        public RoleCompletion(TypeRole type) {
            this.value = type.getClass().getSimpleName();
            pattern = type.getPattern().getClass().getSimpleName();
        }

        @Override
        public String getValue() {
            return value;
        }

        @Override
        public String getMessage() {
            return "Possible role from pattern: " + pattern;
        }
    }

}
